package org.pay.union.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.net.Ipv4Util;
import cn.hutool.core.text.csv.CsvReader;
import cn.hutool.core.text.csv.CsvRow;
import cn.hutool.core.text.csv.CsvUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import org.pay.union.bean.MerchantBean;
import org.pay.union.core.PlatForm;
import org.pay.union.dto.bill.BillDetailDto;
import org.pay.union.dto.bill.BillDto;
import org.pay.union.enums.BillServiceEnum;
import org.pay.union.enums.BillTypeEnum;
import org.pay.union.enums.TradeTypeEnum;
import org.pay.union.param.req.UnifiedTradeNativeReq;
import org.pay.union.param.req.UnifiedTradeRefundReq;
import org.pay.union.param.req.UnifiedTradeReq;
import org.pay.union.param.resp.*;
import org.pay.union.util.SignUtils;
import org.pay.union.util.XmlUtils;

import java.io.StringReader;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.stream.Collectors;

@AllArgsConstructor
public class PayService extends PlatForm {
    /**
     * 配置文件
     */
    private MerchantBean merchantBean;

    /**
     * @param unifiedTradeReq
     * @returnb
         * @see <a href="https://up.95516.com/open/openapi/doc?index_1=2&index_2=1&chapter_1=274&chapter_2=292">提交被扫支付API</a>
     * 业务功能
     * <p>
     * 收银员使用扫码设备读取用户微信/支付宝/云闪付等APP付款码以后，二维码或条码信息传送至商户收银台，由商户收银台或者商户后台调用该接口发起支付对用户进行收款
     * <p>
     * <p>
     * <p>
     * 交互模式
     * <p>
     * 请求：后台请求交互模式
     * <p>
     * 返回结果：后台请求交互模式
     * <p>
     * <p>
     * <p>
     * 关于调用支付接口后相关情况的处理方案：当调用扣款接口返回支付中或未知状态，需要调用查询接口查询订单实际支付状态 。 当遇到用户超过日限额需要输入密码返回“支付中”的状态，建议 5 秒调一次查询，调用 6 次后还未成功作支付超时处理。
     * <p>
     * <p>
     * ①支付请求后：status和result_code字段返回都为0时，交易获得终态，通过pay_result字段判定订单是否支付成功 ；
     * <p>
     * 另外银联二维码交易返回的err_code为两位数字，含义较特殊：00 表示交易 成功，04和06 代表交易需要继续查询，其他数字都是明确代表交易失败。
     * <p>
     * err_code取值为USERPAYING代表用户输密中，需要继续查询；
     * <p>
     * ②支付请求后：返回的参数need_query为Y或没有该参数返回时，必须调用订单查询接口进行支付结果确认（查询接口调用建议参照第④点）；
     * <p>
     * ③支付请求后：返回的参数need_query为N时，可明确为失败订单；
     * <p>
     * ④调用订单查询接口建议：查询6次每隔5秒查询一次（具体的查询次数和时间也可自定义，建议查询时间不低于30秒）6次查询完成，接口仍未返回成功标识(即查询接口返回的trade_state不是SUCCESS)则调用撤销接口；
     */
    public UnifiedTradeMicropayResp unifiedTradeMicropay(UnifiedTradeReq unifiedTradeReq) {
        SortedMap<String, Object> map = XmlUtils.generateMap();
        //接口类型
        map.put("service", "unified.trade.micropay");
        //商户号
        map.put("mch_id", merchantBean.getMchId());
        //商户订单号
        map.put("out_trade_no", StrUtil.isBlank(unifiedTradeReq.getOutTradeNo())? IdUtil.getSnowflakeNextIdStr():unifiedTradeReq.getOutTradeNo());
        //商户及商品名称
        map.put("body", StrUtil.isNotBlank(unifiedTradeReq.getBody())?unifiedTradeReq.getBody():DEFAULT_BODY);
        //总金额，以分为单位，不允许包含任何字、符号
        map.put("total_fee", StrUtil.toString(unifiedTradeReq.getTotalFee()));
        //终端IP
        map.put("mch_create_ip", StrUtil.isBlank(unifiedTradeReq.getMchCreateIp()) ? Ipv4Util.LOCAL_IP : unifiedTradeReq.getMchCreateIp());
        //扫码支付授权码， 设备读取用户展示的条码或者二维码信息
        map.put("auth_code", unifiedTradeReq.getAuthCode());
        //商户侧受理终端信息，字段详细说明参考统一被扫其他扩展字段说明。
        map.put("terminal_info", unifiedTradeReq.getTerminalInfo());
        //随机字符串，不长于 32 位
        map.put("nonce_str", String.valueOf(System.nanoTime()));
        //签名
        map.put("sign", SignUtils.getSign(map, merchantBean.getKey()));
        HttpResponse httpResponse = createQraPost().body(XmlUtils.parseXML(map)).execute();
        String body = httpResponse.body();
        return xmlToBean(body, UnifiedTradeMicropayResp.class);
    }

    /**
     * @param outTradeNo 外部订单号
     * @return
     * @see <a href="https://up.95516.com/open/openapi/doc?index_1=2&index_2=1&chapter_1=274&chapter_2=293">查询订单API</a>
     * 查询订单API
     * 业务功能
     * <p>
     * 根据商户订单号或者平台订单号查询平台的具体订单信息。
     * <p>
     * 交互模式
     * <p>
     * 后台系统调用交互模式
     * <p>
     * <p>
     * <p>
     * 调用订单查询接口建议：查询6次每隔5秒查询一次（具体的查询次数和时间也可自定义，建议查询时间不低于30秒）6次查询完成，接口仍未返回成功标识(即查询接口返回的trade_state不是SUCCESS)则调用撤销接口；
     */
    public BaseQueryResp unifiedTradeQuery(String outTradeNo) {
        SortedMap<String, Object> map = XmlUtils.generateMap();
        //接口类型
        map.put("service", "unified.trade.query");
        //商户订单号
        map.put("out_trade_no", outTradeNo);
        //商户号
        map.put("mch_id", merchantBean.getMchId());
        //随机字符串，不长于 32 位
        map.put("nonce_str", String.valueOf(System.nanoTime()));
        //签名
        map.put("sign", SignUtils.getSign(map, merchantBean.getKey()));
        HttpResponse httpResponse = createQraPost().body(XmlUtils.parseXML(map)).execute();
        String body = httpResponse.body();
        return xmlToBean(body, BaseQueryResp.class);
    }

    /**
     * @param outTradeNo
     * @return
     * @see <a href="https://up.95516.com/open/openapi/doc?index_1=2&index_2=1&chapter_1=274&chapter_2=294">撤销订单API</a>
     * <p>
     * 撤销订单API
     * 业务功能
     * <p>
     * 当支付返回失败，或收银系统超时需要取消交易，可以调用该接口。接口逻辑 ： 支付失败的关单，支付成功的撤销支付。
     * <p>
     * 注意：5分钟内的订单才可以撤销，其他正常支付的单如需实现相同功能请调用退款接口。
     * <p>
     * 调用支付接口后请勿立即调用撤销订单接口，建议支付后至少15s后再调用撤销订单接口。
     */
    public UnifiedMicropayReverseResq unifiedMicropayReverse(String outTradeNo) {
        SortedMap<String, Object> map = XmlUtils.generateMap();
        //接口类型
        map.put("service", "unified.micropay.reverse");
        //商户订单号
        map.put("out_trade_no", outTradeNo);
        //商户号
        map.put("mch_id", merchantBean.getMchId());
        //随机字符串，不长于 32 位
        map.put("nonce_str", String.valueOf(System.nanoTime()));
        //签名
        map.put("sign", SignUtils.getSign(map, merchantBean.getKey()));
        HttpResponse httpResponse = createQraPost().body(XmlUtils.parseXML(map)).execute();
        String body = httpResponse.body();
        return xmlToBean(body, UnifiedMicropayReverseResq.class);
    }

    /**
     * @param unifiedTradeRefundReq
     * @return
     * @see <a href="https://up.95516.com/open/openapi/doc?index_1=2&index_2=1&chapter_1=274&chapter_2=295">申请退款api</a>
     * 申请退款API
     * 业务功能
     * 商户针对某一个已经成功支付的订单发起退款，操作结果在同一会话中同步返回。
     * <p>
     * 一、退款方式
     * 目前只支持原路返回退款
     * <p>
     * 说明：退到银行卡则是非实时的，每个银行的处理速度不同，一般发起退款后1-3个工作日内到账。
     * <p>
     * 同一笔单的部分退款需要设置相同的订单号和不同的 out_refund_no 。一笔退款失败后重新提交，要采用原来 的out_refund_no。总退款金额不能超过用户实际支付金额(现金券金额不能退款)
     * <p>
     * 二、退款限制
     * 商户在退款操作时应该注意退款限制，避免发起不会成功的退款请求，下面是主要的退款限制：
     * <p>
     * 1.在平台系统中，只要退款累计金额不超过交易单支付总额，一笔交易单可以多次退款，退款申请单号（退款接口中有此参数）唯一确定一       次退款，而不是交易单号确定一次退款。退款申请单号由商户生成，所以商户一定要保证退款申请单的唯一性。商家在退款过程中要特别       注意，只有在能确定退款失败的情况下，才能重新发起另一笔退款。一笔退款失败后重新提交，请不要更换退款单号，请使用原商户退款       单号。
     * <p>
     * 2.请求频率限制：150qps，即每秒钟正常的申请退款请求次数不超过150次。
     * <p>
     * 3.错误或无效请求频率限制：6qps，即每秒钟异常或错误的退款申请请求不超过6次。
     * <p>
     * 4.每个支付订单的部分退款次数不能超过50次。
     */
    public UnifiedTradeRefundResq unifiedTradeRefund(UnifiedTradeRefundReq unifiedTradeRefundReq) {
        SortedMap<String, Object> map = XmlUtils.generateMap();
        //接口类型
        map.put("service", "unified.trade.refund");
        //商户订单号
        map.put("out_trade_no", unifiedTradeRefundReq.getOutTradeNo());
        //商户退款单号
        map.put("out_refund_no", unifiedTradeRefundReq.getOutRefundNo());
        //订单总金额，单位为分
        map.put("total_fee", StrUtil.toString(unifiedTradeRefundReq.getTotalFee()));
        //退款总金额,单位为分,可以做部分退款
        map.put("refund_fee", StrUtil.toString(unifiedTradeRefundReq.getRefundFee()));
        //操作员帐号,默认为商户号
        map.put("op_user_id", merchantBean.getMchId());
        //商户号
        map.put("mch_id", merchantBean.getMchId());
        //随机字符串，不长于 32 位
        map.put("nonce_str", String.valueOf(System.nanoTime()));
        //签名
        map.put("sign", SignUtils.getSign(map, merchantBean.getKey()));
        HttpResponse httpResponse = createQraPost().body(XmlUtils.parseXML(map)).execute();
        String body = httpResponse.body();
        return xmlToBean(body, UnifiedTradeRefundResq.class);
    }

    /**
     * @param outTradeNo
     * @param outRefundNo
     * @return
     * @see <a href="https://up.95516.com/open/openapi/doc?index_1=2&index_2=1&chapter_1=274&chapter_2=296">退款查询</a>
     * 提交退款申请后， 通过调用该接口查询退款状态。 退款有一定延时， 请在 3 个工作日后
     * <p>
     * 重新查询退款状态。
     */
    public UnifiedTradeRefundQueryResq unifiedMicropayReverseQuery(String outTradeNo, String outRefundNo) {
        SortedMap<String, Object> map = XmlUtils.generateMap();
        //接口类型
        map.put("service", "unified.trade.refundquery");
        //商户订单号
        map.put("out_trade_no", outTradeNo);
        //商户退款单号，32个字符内、可包含字母,确保在商户系统唯一。
        map.put("out_refund_no", outRefundNo);
        //商户号
        map.put("mch_id", merchantBean.getMchId());
        //随机字符串，不长于 32 位
        map.put("nonce_str", String.valueOf(System.nanoTime()));
        //签名
        map.put("sign", SignUtils.getSign(map, merchantBean.getKey()));
        HttpResponse httpResponse = createQraPost().body(XmlUtils.parseXML(map)).execute();
        String body = httpResponse.body();
        return xmlToBean(body, UnifiedTradeRefundQueryResq.class);
    }

    /**
     * @param unifiedTradeReq
     * @return
     * @see <a href="https://up.95516.com/open/openapi/doc?index_1=1&index_2=1&chapter_1=235&chapter_2=253">统一下单API</a>
     * 初始化扫码请求，通过该请求生成二维码来进行扫码支付。
     */
    public UnifiedTradeNativeResp unifiedTradeNative(UnifiedTradeNativeReq unifiedTradeReq) {
        SortedMap<String, Object> map = XmlUtils.generateMap();
        //接口类型
        map.put("service", "unified.trade.native");
        //商户订单号
        map.put("out_trade_no", StrUtil.isBlank(unifiedTradeReq.getOutTradeNo())? IdUtil.getSnowflakeNextIdStr():unifiedTradeReq.getOutTradeNo());
        //商户号
        map.put("mch_id", merchantBean.getMchId());
        //随机字符串，不长于 32 位
        map.put("nonce_str", String.valueOf(System.nanoTime()));
        //商户及商品名称
        map.put("body", StrUtil.isNotBlank(unifiedTradeReq.getBody())?unifiedTradeReq.getBody():DEFAULT_BODY);
        //终端IP
        map.put("mch_create_ip", StrUtil.isBlank(unifiedTradeReq.getMchCreateIp()) ? Ipv4Util.LOCAL_IP : unifiedTradeReq.getMchCreateIp());
        //总金额
        map.put("total_fee", StrUtil.toString(unifiedTradeReq.getTotalFee()));
        //接收平台通知的URL，需给绝对路径，255字符内格式如:http://wap.tenpay.com/tenpay.asp，确保平台能通过互联网访问该地址
        map.put("notify_url", unifiedTradeReq.getNotifyUrl());
        //签名
        map.put("sign", SignUtils.getSign(map, merchantBean.getKey()));
        HttpResponse httpResponse = createQraPost().body(XmlUtils.parseXML(map)).execute();
        String body = httpResponse.body();
        return xmlToBean(body, UnifiedTradeNativeResp.class);
    }

    /**
     * @param billDate
     * @param billServiceEnum
     * @param billTypeEnum
     * @return
     * @see <a href="https://up.95516.com/open/openapi/doc?index_1=26&index_2=1&chapter_1=622&chapter_2=626">下载对账单</a>
     * 下载对账单
     */
    public BillDto bill(String billDate, BillServiceEnum billServiceEnum, BillTypeEnum billTypeEnum) throws Exception {
        SortedMap<String, Object> map = XmlUtils.generateMap();
        //接口类型
        map.put("service", (billServiceEnum == null ? BillServiceEnum.merchant : billServiceEnum).getVal());
        //账单日期格式：yyyyMMdd(如：20150101)
        map.put("bill_date", billDate);
        //账单类型
        map.put("bill_type", ((billTypeEnum == null) ? BillTypeEnum.ALL : billTypeEnum).name());
        //商户号
        map.put("mch_id", merchantBean.getMchId());
        //随机字符串，不长于 32 位
        map.put("nonce_str", String.valueOf(System.nanoTime()));
        //签名
        map.put("sign", SignUtils.getSign(map, merchantBean.getKey()));
        HttpResponse httpResponse = createUpPost().body(XmlUtils.parseXML(map)).execute();
        String body = httpResponse.body();
        ResultJsonResp jsonResp;
        try {
            jsonResp = JSONUtil.toBean(body, ResultJsonResp.class);
        } catch (Exception e) {
            CsvReader reader = CsvUtil.getReader(new StringReader(body));
            List<CsvRow> csvRowList = reader.stream().skip(1).collect(Collectors.toList());
            //数据集合
            List<BillDetailDto> detailRespList = csvRowList.stream().map(row -> toBillDetail(row.getRawList())).filter(Objects::nonNull).collect(Collectors.toList());
            BillDto billDto = toBill(csvRowList.stream().reduce((a, b) -> b).get().getRawList());
            billDto.setBillDetailList(detailRespList);
            return billDto;
        }
        if (StrUtil.isNotBlank(jsonResp.getMessage())) {
            throw new Exception(jsonResp.getMessage());
        }
        return null;
    }

    public static BillDto toBill(List<String> rawList) {
        BillDto billDto = new BillDto();
        billDto.setTotalTradeNum((trimSpec(rawList.get(0))));
        billDto.setTotalTradeFee((trimSpec(rawList.get(1))));
        billDto.setTotalRefundFee((trimSpec(rawList.get(2))));
        billDto.setTotalComRefundFee((trimSpec(rawList.get(3))));
        billDto.setTotalServiceFee((trimSpec(rawList.get(4))));
        billDto.setTotalRealFee((trimSpec(rawList.get(5))));
        return billDto;
    }

    public static BillDetailDto toBillDetail(List<String> rawList) {
        if (CollUtil.size(rawList) != 38) {
            return null;
        }
        BillDetailDto billDetailResp = new BillDetailDto();
        billDetailResp.setTradeTime(trimSpec(rawList.get(0)));
        billDetailResp.setAppId(trimSpec(rawList.get(1)));
        billDetailResp.setThirdMerId(trimSpec(rawList.get(2)));
        billDetailResp.setMchId(trimSpec(rawList.get(3)));
        billDetailResp.setSubMerNo(trimSpec(rawList.get(4)));
        billDetailResp.setDeviceNo(trimSpec(rawList.get(5)));
        billDetailResp.setPtOrderNo(trimSpec(rawList.get(6)));
        billDetailResp.setUnionPayOrderNo(trimSpec(rawList.get(7)));
        billDetailResp.setMerOrderNo(trimSpec(rawList.get(8)));
        billDetailResp.setUserTag(trimSpec(rawList.get(9)));
        billDetailResp.setTradeType(trimSpec(rawList.get(10)));
        billDetailResp.setTradeStatus(trimSpec(rawList.get(11)));
        billDetailResp.setPayBank(trimSpec(rawList.get(12)));
        billDetailResp.setFeeType(trimSpec(rawList.get(13)));
        billDetailResp.setTotalFee(trimSpec(rawList.get(14)));
        billDetailResp.setComFee(trimSpec(rawList.get(15)));
        billDetailResp.setPtRefundNo(trimSpec(rawList.get(16)));
        billDetailResp.setRefundNo(trimSpec(rawList.get(17)));
        billDetailResp.setRefundFee(trimSpec(rawList.get(18)));
        billDetailResp.setComRefundFee(trimSpec(rawList.get(19)));
        billDetailResp.setRefundTypeS(trimSpec(rawList.get(20)));
        billDetailResp.setRefundStatus(trimSpec(rawList.get(21)));
        billDetailResp.setCommName(trimSpec(rawList.get(22)));
        billDetailResp.setAttach(trimSpec(rawList.get(23)));
        billDetailResp.setServiceFee(trimSpec(rawList.get(24)));
        billDetailResp.setRate(trimSpec(rawList.get(25)));
        billDetailResp.setTerminalType(trimSpec(rawList.get(26)));
        billDetailResp.setRecondTag(trimSpec(rawList.get(27)));
        billDetailResp.setStoreNo(trimSpec(rawList.get(28)));
        billDetailResp.setMerName(trimSpec(rawList.get(29)));
        billDetailResp.setOpt(trimSpec(rawList.get(30)));
        billDetailResp.setSubMerId(trimSpec(rawList.get(31)));
        billDetailResp.setDiscountFee(trimSpec(rawList.get(32)));
        billDetailResp.setRealFee(trimSpec(rawList.get(33)));
        billDetailResp.setSettleFee(trimSpec(rawList.get(34)));
        billDetailResp.setExt3(trimSpec(rawList.get(35)));
        billDetailResp.setExt4(trimSpec(rawList.get(36)));
        billDetailResp.setExt5(trimSpec(rawList.get(37)));
        TradeTypeEnum tradeTypeEnum= TradeTypeEnum.codeOf(billDetailResp.getTradeType());
        if(tradeTypeEnum!=null){
            billDetailResp.setTradeTypeEnum(tradeTypeEnum);
        }
        return billDetailResp;
    }

    public static final String trimSpec(String str){
        return (StrUtil.isBlank(str))?null:str.replaceAll("`","");
    }
    public static HttpRequest createQraPost() {
        return HttpUtil.createPost(QRA_GETEWAY)
                .header("Content-Type", "text/xml;charset=ISO-8859-1");
    }

    public static HttpRequest createUpPost() {
        return createQraPost().setUrl(UP_GETEWAY);
    }

    /**
     * 将xml转换为bean
     *
     * @param xml
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T xmlToBean(String xml, Class<T> clazz) {
        if (StrUtil.isBlank(xml)) {
            return null;
        }
        Map<String, Object> xmlMap = XmlUtil.xmlToMap(xml);
        return BeanUtil.toBean(xmlMap, clazz);
    }
}
